package com.brayan.melinxa.logic.shadowers.local2local;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.inject.Inject;

import com.brayan.melinxa.SystemStatus;
import com.brayan.melinxa.logic.exceptions.MonitoringException;
import com.brayan.melinxa.logic.exceptions.ShadowerInitalisationException;
import com.brayan.melinxa.logic.execution.Copy;
import com.brayan.melinxa.logic.execution.Delete;
import com.brayan.melinxa.logic.execution.Executor;
import com.brayan.melinxa.logic.shadowers.Shadower;
import com.brayan.melinxa.logic.shadowers.ShadowerObservable;
import com.brayan.melinxa.logic.shadowers.credentials.LoginCredentials;
import com.brayan.melinxa.logic.shadowers.locations.LocalDirectory;
import com.brayan.melinxa.logic.shadowers.locations.LocationPair;
import com.brayan.melinxa.logic.shadowers.locations.ShadowerLocation;

/**
 * This class does the major work in Melinxa. It mirrors (shadows) the data of
 * the provided source directory.
 * 
 * This class must implement from the shadower like every class which is capable
 * of shading data from a source to a destination.
 * 
 * Currently there is no authorization and authentication implemented. TODO for
 * other OS than Windows. In particular windows.
 * 
 * @author Brayan Zimmerli
 * 
 */
public class LocalDirectoryShadower extends Shadower {
	@Inject
	Logger logger;

	static LocalDirectoryShadower localDirShadower;

	private final String SHADOWER_TYPE_DESCRIPTION = "A local directory shadower copying from a local directory to a remote directory.";
	private final String SHADOWER_TYPE = "Local2Local";

	// ---Inner-Behaviour and States---
	
	/** Boolean to indicate if the for Shadower was to shutdown. */
	boolean isToShutDown = false;

	private WatchService watcher;
	private Map<WatchKey, Path> keys;
	private boolean recursive = true;

	/**
	 * The constructor omits to throw an exception if parameters are set to
	 * null.
	 * 
	 * @param localDirSrc
	 * @param localDirDest
	 * @throws MonitoringException
	 */
	private LocalDirectoryShadower(LocationPair locationPair) throws ShadowerInitalisationException {
		super.setLocationPair(locationPair);
		if (locationPair == null) {
			activate(false);
		} else {
			// TODO set to deactivated, if it fails to resolve to a file.
			try {
				//init();
				activate(true);
			} catch (ShadowerInitalisationException sie) {
				System.out.println(sie.getMessage());
				// TODO USE LOGGER
				// logger.warning(mex.getMessage());
				throw sie;
			}
		}
	}

	public static LocalDirectoryShadower getInstance(LocationPair locationPair)
			throws ShadowerInitalisationException {
		if (localDirShadower == null) {
			try {
				localDirShadower = new LocalDirectoryShadower(locationPair);
			} catch (ShadowerInitalisationException sie) {
				// TODO user logger
				throw sie;
			}
		}
		return localDirShadower;
	}

	/**
	 * Using this method wont do more than {@link #getInstance()}. Since
	 * LoginCredentials are not required for the {@link LocalDirectoryShadower}.
	 * 
	 * @param locationPair
	 * @param loginCredentials
	 * @return
	 * @throws ShadowerInitalisationException
	 */
	public static LocalDirectoryShadower getInstance(LocationPair locationPair,
			LoginCredentials loginCredentials) throws ShadowerInitalisationException {
		return getInstance(locationPair);
	}

	@Override
	public ShadowerLocation getSource() {
		return super.locationPair.getSrcLocation();
	}

	@Override
	public ShadowerLocation getDestination() {
		return super.locationPair.getDestLocation();
	}

	private String getSourceString() {
		LocalDirectory localDir = (LocalDirectory) super.locationPair.getSrcLocation();
		return localDir.getDirString();
	}

	private String getDestinationString() {
		LocalDirectory localDir = (LocalDirectory) super.locationPair.getDestLocation();
		return localDir.getDirString();
	}

	private Path getSourcePath() {
		LocalDirectory localDir = (LocalDirectory) super.locationPair.getSrcLocation();
		return localDir.getPathObject();
	}

	private Path getDestinationPath() {
		LocalDirectory localDir = (LocalDirectory) super.locationPair.getDestLocation();
		return localDir.getPathObject();
	}

	/**
	 * Initialize the Watcher service and the watch key hash map.
	 */
	protected void init() throws ShadowerInitalisationException {
		if (keys == null)
			keys = new HashMap<WatchKey, Path>();
		// verify it's an existing File.
		if (!isReady()) {
			throw new ShadowerInitalisationException(
					"The watcher is not ready to be initalized. Verify if you have provided a valid location pair.");
		}
		// Get the single Watchservice
		try {
			watcher = FileSystems.getDefault().newWatchService();
		} catch (IOException ioe) {
			throw new ShadowerInitalisationException("Failed to retrieve the watch service. "
					+ ioe.getMessage());
		}
		super.executor = Executor.getInstance();

		// Monitor from this path downwards
		// TODO read this path(s) from propertie file
		try {
			registerAll(getSourcePath());
		} catch (IOException e) {
			// TODO use logger
			System.out.println("Failed to register the entire directory.");
		}
	}

	private void registerDirectory(Path dir) throws IOException {
		WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
		Path prev = keys.get(key);
		if (prev == null) {
			System.out.format("register: %s\n", dir);
		} else {
			if (!dir.equals(prev)) {
				System.out.format("update: %s -> %s\n", prev, dir);
			}
		}
		keys.put(key, dir);
	}

	/**
	 * Register the given directory, and all its sub-directories, with the
	 * WatchService.
	 */
	private void registerAll(final Path srcDirPath) throws IOException {
		// register directory and sub-directories
		Files.walkFileTree(srcDirPath, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
					throws IOException {
				registerDirectory(dir);
				return FileVisitResult.CONTINUE;
			}
		});
	}

	@SuppressWarnings("unchecked")
	static <T> WatchEvent<T> cast(WatchEvent<?> event) {
		return (WatchEvent<T>) event;
	}

	/**
	 * Invokes the monitoring process. Never allow let this method run out. Make
	 * sure it keeps looping.
	 */
	void processWatchService() {

		for (;;) {
			// wait for key to be signalled
			WatchKey key;
			try {
				// Non-polling method. Don't use watcher.poll().
				key = watcher.take();
				// If the shutdown flag was set, exit the loop.
				if (isToShutDown) {
					notifyObserversOfStateChangedTo(SystemStatus.CLOSED);
					return;
				}
			} catch (InterruptedException x) {
				notifyObserversOfStateChangedTo(SystemStatus.ERROR);
				// TODO use logger
				System.err.print("The Thread is interrupted and stopped.");
				return;
			}

			Path dir = keys.get(key);
			if (dir == null) {
				// TODO use logger
				System.err.println("WatchKey not recognized! Key is ingnored from now on.");
				continue;
			}

			for (WatchEvent<?> event : key.pollEvents()) {
				WatchEvent.Kind kind = event.kind();

				// TODO CAUTION, THIS WILL UNREGISTER THE KEY!!!
				if (kind == OVERFLOW) {
					continue;
				}

				// Context for directory entry event is the file name of
				// entry
				WatchEvent<Path> ev = cast(event);
				Path name = ev.context();
				Path child = dir.resolve(name);

				// print out event
				System.out.format("%s: %s\n", event.kind().name(), child);

				// If directory is created, and watching recursively, then
				// register it and its sub-directories
				if (recursive && (kind == ENTRY_CREATE)) {
					try {
						if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
							registerAll(child);
						}
					} catch (IOException x) {
						// ignore to keep sample readbale
					}
					// Invoke the copy executable.
					if (isActivated())
						executor.addExecutable(new Copy(child, getSourcePath(),
								getDestinationPath()));
				} else if (kind == ENTRY_MODIFY) {
					// TODO anything to do in this case?
					if (isActivated())
						System.out.println("MODIFICATION EVENT OCCURED: " + child.toString());
				} else if (kind == ENTRY_DELETE) {
					// Invoke the delete executable.
					if (isActivated())
						executor.addExecutable(new Delete(child, getSourcePath(),
								getDestinationPath()));
				}
			}

			// reset key and remove from set if directory no longer
			// accessible
			boolean valid = key.reset();
			if (!valid) {
				System.out.println("Remove key " + key.watchable().toString());
				keys.remove(key);
				// all directories are inaccessible
				if (keys.isEmpty()) {
					System.out.println("KEYS IS EMPTY BREAKING THE WATCH-LOOP!");
					break;
				}
			}

		}
	}

	@Override
	public boolean isActivated() {
		return super.isActivated;
	}

	public void run() {
		try {
			do {
				if (isActivated()) {
					processWatchService();
					// if this watchprocess ever runs out, break the loop and
					// leave the application
					break;
				}				
			} while (!isActivated());
			// TODO use logger
			System.out.println("The watcher has been closed and will now try to close the shadower, too.");
		} catch (ClosedWatchServiceException cwse) {
			// TODO use logger
			System.out
					.println("The watcher has been closed. But there was still a WatchKey in the poll or take. This error is non-critical. "
							+ cwse.getMessage());
		}
	}

	/**
	 * Use to gracefully shutdown the data shadowing. The observers are notified
	 * as soon as the loop {@link #processWatchService()} recognizes the
	 * shutdown. Don't trigger the notification manually.
	 */
	public void shutdown() {
		notifyObserversOfStateChangedTo(SystemStatus.CLOSED);
		isToShutDown = true;		
	}

	@Override
	public void registerObserverOfStateChange(ShadowerObservable observer) {
		listOfObservers.add(observer);
	}

	@Override
	public void deregisterObserverOfStateChange(ShadowerObservable observer) {
		listOfObservers.remove(observer);
	}

	@Override
	public void notifyObserversOfStateChangedTo(SystemStatus state) {
		for (ShadowerObservable observer : listOfObservers) {
			observer.shadowerStateChangedTo(state);
		}
	}

	@Override
	public String getTypeOfShadower() {
		return SHADOWER_TYPE;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * Checks if source and destination is available and if they map to a valid
	 * directory.
	 */
	@Override
	public boolean isReady() {
		// check if source and destination directory exists
		if (locationPair == null || !locationPair.isPairNotNull()) {
			// TODO use logger
			System.out.println("The shadower is not ready. The location pair is invalid.");
			return false;
		} else {
			LocalDirectory src = (LocalDirectory) locationPair.getSrcLocation();
			LocalDirectory dest = (LocalDirectory) locationPair.getDestLocation();
			if (src.isValid() & dest.isValid())
				return true;
			else
				return false;
		}
		// no need to check credentials
		// no need to check for filters
	}

}
